Skip to main content
Iquea Commerce is a full-featured e-commerce platform designed specifically for furniture retail. The platform provides a complete suite of tools for managing products, processing orders, and delivering an exceptional customer experience.

Core Features

Product Catalog Management

Comprehensive product management system with categories, SKUs, pricing, dimensions, stock tracking, and featured product highlighting.

JWT Authentication

Secure token-based authentication with role-based access control for administrators and customers.

Shopping Cart

Persistent shopping cart with localStorage synchronization, quantity management, and real-time total calculations.

Order Management

Complete order processing system with status tracking, unique reference codes, and order history.

Advanced Search

Multi-criteria product search including name filtering, price range queries, and featured product discovery.

Admin Dashboard

Full administrative control for product, category, and user management with role-based permissions.

Authentication & Security

The platform implements enterprise-grade security using JWT (JSON Web Tokens) and Spring Security.

JWT Token Generation

When users register or login, the backend generates a JWT token containing their email and role:
AuthController.java
@PostMapping("/login")
public ResponseEntity<TokenDTO> login(@RequestBody LoginDTO dto) {
    Usuario usuario = usuarioRepository.findByEmailValue(dto.getEmail())
            .orElseThrow(() -> new RuntimeException("Usuario no encontrado"));

    if (!passwordEncoder.matches(dto.getPassword(), usuario.getPassword())) {
        throw new IllegalArgumentException("Contraseña incorrecta");
    }

    String token = jwtUtil.generarToken(
            usuario.getEmail().getValue(),
            usuario.getRol().name()
    );
    return ResponseEntity.ok(new TokenDTO(token));
}

JWT Utility Implementation

The JWT utility handles token generation, validation, and claims extraction:
JwtUtil.java
public String generarToken(String email, String rol) {
    return Jwts.builder()
            .subject(email)
            .claim("rol", rol)
            .issuedAt(new Date())
            .expiration(new Date(System.currentTimeMillis() + EXPIRATION_MS))
            .signWith(key)
            .compact();
}

public boolean esValido(String token) {
    try {
        parsear(token);
        return true;
    } catch (JwtException | IllegalArgumentException e) {
        return false;
    }
}

Frontend Authentication Context

The React frontend maintains authentication state using Context API:
AuthContext.tsx
export function AuthProvider({ children }: { children: ReactNode }) {
    const [token, setTokenState] = useState<string | null>(
        () => localStorage.getItem('token')
    );

    const decoded = token ? jwtDecode<JwtPayload>(token) : null;
    const email = decoded?.sub ?? null;
    const rol = decoded?.rol ?? null;
    const isAuthenticated = !!token && (decoded?.exp ?? 0) > Date.now() / 1000;

    function setToken(newToken: string | null) {
        if (newToken) {
            localStorage.setItem('token', newToken);
        } else {
            localStorage.removeItem('token');
        }
        setTokenState(newToken);
    }

    return (
        <AuthContext.Provider value={{ token, email, rol, isAuthenticated, setToken, logout }}>
            {children}
        </AuthContext.Provider>
    );
}

Product Management

Product Model

Products are stored with comprehensive metadata including dimensions, pricing, and inventory:
Producto.java
@Entity
@Table(name = "Producto")
public class Producto {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long producto_id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "categoria_id")
    private Categorias categoria;

    @Column(name = "nombre", length = 155, nullable = false)
    private String nombre;

    @Column(name = "sku", unique = true, nullable = false, length = 50)
    private String sku;

    @Embedded
    private Precio precio;

    @Column(name = "es_destacado")
    private boolean es_destacado;

    @Embedded
    private Dimensiones dimensiones;

    @Column(name = "stock", nullable = false)
    private Integer stock = 0;

    @Column(name = "imagen_url")
    private String imagenUrl;
}

Product Search & Filtering

The platform provides multiple search endpoints for flexible product discovery:
ProductoController.java
@GetMapping("/buscar")
public ResponseEntity<List<ProductoDetalleDTO>> buscarPorNombre(@RequestParam String nombre) {
    List<Producto> productos = productoService.obtenerProductoPorLetra(nombre);
    return ResponseEntity.ok(productoMapper.toDTOlist(productos));
}

@GetMapping("/precio")
public ResponseEntity<List<ProductoDetalleDTO>> porRangoPrecio(
        @RequestParam BigDecimal min,
        @RequestParam BigDecimal max) {
    List<Producto> productos = productoService.obtenerProductoPorRango(min, max);
    return ResponseEntity.ok(productoMapper.toDTOlist(productos));
}

@GetMapping("/destacados")
public ResponseEntity<List<ProductoDetalleDTO>> destacados() {
    List<Producto> productos = productoService.obtenerProductosPorDestacado(true);
    return ResponseEntity.ok(productoMapper.toDTOlist(productos));
}

Shopping Cart System

The shopping cart is implemented client-side with localStorage persistence for seamless user experience.

Cart Context Implementation

CartContext.tsx
export function CartProvider({ children }: { children: ReactNode }) {
    const [cart, setCart] = useState<CartItem[]>(() => {
        const saved = localStorage.getItem('cart');
        return saved ? JSON.parse(saved) : [];
    });

    useEffect(() => {
        localStorage.setItem('cart', JSON.stringify(cart));
    }, [cart]);

    const addToCart = (product: Producto, quantity: number) => {
        setCart((prev) => {
            const existing = prev.find((item) => item.producto.producto_id === product.producto_id);
            if (existing) {
                return prev.map((item) =>
                    item.producto.producto_id === product.producto_id
                        ? { ...item, cantidad: item.cantidad + quantity }
                        : item
                );
            }
            return [...prev, { producto: product, cantidad: quantity }];
        });
    };

    const total = cart.reduce((sum, item) => sum + (item.producto.precioCantidad * item.cantidad), 0);
    const count = cart.reduce((sum, item) => sum + item.cantidad, 0);

    return (
        <CartContext.Provider value={{ cart, addToCart, removeFromCart, updateQuantity, clearCart, total, count }}>
            {children}
        </CartContext.Provider>
    );
}
The cart persists across browser sessions using localStorage, ensuring customers don’t lose their selections.

Order Management

Order Model with Auto-Generated References

Orders automatically generate unique reference codes for tracking:
Pedido.java
@Entity
@Table(name = "Pedidos")
public class Pedido {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long pedido_id;

    @ManyToOne
    @JoinColumn(name = "usuario_id", nullable = false)
    private Usuario usuario;

    @Column(name = "fecha_pedido", nullable = false)
    private LocalDateTime fechaPedido;

    @Enumerated(EnumType.STRING)
    @Column(name = "estado", nullable = false)
    private EstadoPedido estado;

    @Column(name = "referencia", unique = true, nullable = false, length = 10)
    private String referencia;

    @OneToMany(mappedBy = "pedido", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Detalle_pedido> detalles = new ArrayList<>();

    @PrePersist
    private void generarReferencia() {
        if (this.referencia == null) {
            this.referencia = generarCodigoAleatorio();
        }
    }

    public BigDecimal calcularTotal() {
        return detalles.stream()
                .map(detalle -> detalle.getPrecioUnitario().multiply(new BigDecimal(detalle.getCantidad())))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

Order Status Tracking

Orders can be filtered by status for efficient order management:
PedidoController.java
@GetMapping("/estado/{estado}")
public ResponseEntity<List<PedidoDetalleDTO>> porEstado(@PathVariable EstadoPedido estado) {
    List<Pedido> pedidos = pedidoService.buscarPorEstado(estado);
    return ResponseEntity.ok(pedidoMapper.toDTOlist(pedidos));
}

Role-Based Access Control

User Roles

The platform supports two user roles:
RolUsuario.java
public enum RolUsuario {
    ADMIN,
    CLIENTE
}

Security Configuration

Spring Security enforces role-based permissions:
SecurityConfig.java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf.disable())
        .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(auth -> auth
            // Public routes
            .requestMatchers(HttpMethod.POST, "/api/auth/**").permitAll()
            .requestMatchers(HttpMethod.GET,  "/api/productos/**").permitAll()
            .requestMatchers(HttpMethod.GET,  "/api/categorias/**").permitAll()
            // Only ADMIN can create/edit/delete products and categories
            .requestMatchers(HttpMethod.POST,   "/api/productos/**").hasRole("ADMIN")
            .requestMatchers(HttpMethod.PUT,    "/api/productos/**").hasRole("ADMIN")
            .requestMatchers(HttpMethod.DELETE, "/api/productos/**").hasRole("ADMIN")
            .requestMatchers(HttpMethod.POST,   "/api/categorias/**").hasRole("ADMIN")
            .requestMatchers(HttpMethod.PUT,    "/api/categorias/**").hasRole("ADMIN")
            .requestMatchers(HttpMethod.DELETE, "/api/categorias/**").hasRole("ADMIN")
            // Everything else requires authentication
            .anyRequest().authenticated()
        )
        .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

    return http.build();
}
All product and category modification endpoints require ADMIN role. Regular customers (CLIENTE) can only browse and purchase.

Category Management

Products are organized into categories for easy navigation:
CategoriaController.java
@RestController
@RequestMapping("/api/categorias")
public class CategoriaController {
    
    @GetMapping
    public ResponseEntity<List<CategoriaDetalleDTO>> listarTodas(){
        List<Categorias> categorias = categoriaService.obtenerTodaslasCategorias();
        return ResponseEntity.ok(categorias.stream().map(categoriaMapper::toDTO).toList());
    }

    @GetMapping("/{nombre}")
    public ResponseEntity<CategoriaDetalleDTO> obtenerPorNombre(@PathVariable String nombre){
        return categoriaService.obtenerCategoriaPorNombre(nombre)
        .map(c-> ResponseEntity.ok(categoriaMapper::toDTO(c)))
        .orElse(ResponseEntity.notFound().build());
    }
}

API Client Integration

The frontend uses a centralized API client with automatic JWT token injection:
client.ts
const BASE_URL = 'http://localhost:8080/api';

function getToken(): string | null {
    return localStorage.getItem('token');
}

function authHeaders(): HeadersInit {
    const token = getToken();
    return {
        'Content-Type': 'application/json',
        ...(token ? { Authorization: `Bearer ${token}` } : {}),
    };
}

export async function apiFetch<T>(
    path: string,
    options: RequestInit = {}
): Promise<T> {
    const res = await fetch(`${BASE_URL}${path}`, {
        ...options,
        headers: {
            ...authHeaders(),
            ...(options.headers ?? {}),
        },
    });

    if (!res.ok) {
        const error = await res.json().catch(() => ({ message: 'Error desconocido' }));
        throw new Error(error.message ?? `Error ${res.status}`);
    }

    return res.json() as Promise<T>;
}

Next Steps

Architecture Overview

Explore the system architecture and technology stack

API Reference

Browse complete API endpoint documentation